Skip to content

feat(github): render live row-copy progress on sharded table lines - #1191

Merged
aparajon merged 4 commits into
mainfrom
armand/strata-live-table-progress
Sep 1, 2026
Merged

feat(github): render live row-copy progress on sharded table lines#1191
aparajon merged 4 commits into
mainfrom
armand/strata-live-table-progress

Conversation

@aparajon

@aparajon aparajon commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Why this matters

A copying table on a sharded apply shows only a state word, while the real copy progress — rows copied, totals, ETA — already sits in the per-shard task rows the driver writes through on every poll. The operator watching the PR gets less information than the system has. This PR puts that live progress on the table line, rendered the same way a single-target apply renders it.

What it does

The table line aggregates its shard tasks at render time — the template stays dumb and the per-shard write path is untouched:

per-shard task rows (rows_copied, rows_total, eta_seconds)
  └─ webhook builder, at render time
      ├─ rows     = sum across shards that have reported
      ├─ percent  = from the summed rows (not averaged shard percents)
      └─ ETA      = the slowest shard's — it bounds the table

Three safety properties worth calling out:

  • The honesty guards carry over from the single-target renderer. A copy that has not reported rows yet renders "⏳ Starting copy..." instead of an empty bar (and a sub-1% copy with rows flowing shows a 1% floor, never the starting form), and copied rows exceeding the estimate render the activity bar with the "so far" count instead of a false percentage.
  • Partial coverage is disclosed, not passed off as the table's total. Until every dispatch wave has started, the sums describe only the reporting shards — so the headline and rows line both name the coverage ("62% (1 of 4 shards)", "across 1 of 4 shards") and the ETA renders as a floor ("ETA: ≥ 3m 15s"), since the unstarted shards can only add rows and time. When plan-time row estimates land, only the denominator seeding changes; the disclosure vocabulary stays.
  • Rows only qualify a table in the copy phase. Checksum, cutover, and other in-flight phases keep their state phrases even when row figures are present, so a checksum never wears a misleading copy bar.

Because the aggregate reads durable task rows, any pod rendering the comment shows the same numbers — there is no in-memory progress state to lose across a lease handover.

Apply In Progress

Schema Change Status — Production

Database: cdb_resolute | Type: Strata | Apply ID: apply-a1b2c3d4e5f6

Applied by @jackjackbits at 2026-01-01 00:00:00 UTC

Status: In Progress — 0 of 1 change applied

Shards: 1 running table copy, 3 waiting for -40

Keyspace cdb_resolute_sharded

mutes: 🟦🟦🟦🟦🟦🟦🟦🟦🟦🟦🟦🟦⬜⬜⬜⬜⬜⬜⬜⬜ 62% (1 of 4 shards)

  • Rows: 914,707 / 1,466,232 across 1 of 4 shards · ETA: ≥ 3m 15s
    └ shards: ◐ -40 62% · ⏳ 40-80 · ⏳ 80-c0 · ⏳ c0-

Last updated: 2026-01-01 00:00:00 UTC (2026-01-01 00:00:00 UTC)

Apply Across Multiple Keyspaces

Schema Change Status — Production

Database: cdb_resolute | Type: Strata | Apply ID: apply-a1b2c3d4e5f6

Applied by @jackjackbits at 2026-01-01 00:00:00 UTC

Status: In Progress — 1 of 4 changes applied

Shards: 1 completed, 1 running table copy, 4 waiting for cdb_resolute_lookup/-

Keyspace cdb_resolute

outcomes: ✅ Complete

Keyspace cdb_resolute_lookup

outcomes_lookup: 🟦🟦🟦🟦🟦⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜ 27%

  • Rows: 540,211 / 2,000,780 · ETA: 8m 0s

Keyspace cdb_resolute_sharded

mutes: ⏳ Queued

VSchema

cdb_resolute_sharded: Pending

Last updated: 2026-01-01 00:00:00 UTC (2026-01-01 00:00:00 UTC)

How it moves us toward the northstar

This closes the per-shard progress loop for sharded targets: the durable per-shard write-through now reaches the operator on the PR as honest table-level copy progress, completing the comment redesign this series set out (#1188#1190).

Opened by Claude (Fable 5).

@aparajon
aparajon force-pushed the armand/strata-table-unit-render branch from a58e873 to 64c36fa Compare August 29, 2026 06:23
@aparajon
aparajon force-pushed the armand/strata-live-table-progress branch 2 times, most recently from 62cf08c to 01658d0 Compare August 30, 2026 02:22
@aparajon
aparajon force-pushed the armand/strata-table-unit-render branch 2 times, most recently from e110697 to f3ecde5 Compare August 30, 2026 02:54
@aparajon
aparajon force-pushed the armand/strata-live-table-progress branch 3 times, most recently from cf7ded3 to d0aa6b4 Compare August 30, 2026 21:54
Base automatically changed from armand/strata-table-unit-render to main August 30, 2026 21:59
A copying table on a sharded apply now shows the same progress bar, rows
line, and ETA a single-target apply shows, aggregated at render time from
the per-shard task rows: rows sum across the shards that have reported,
the ETA is the slowest shard's, and the percentage comes from the summed
rows rather than averaged shard percents. The honesty guards carry over —
a zero copied count renders "Starting copy..." and an exceeded estimate
renders the activity bar with the "so far" count instead of a false
percentage. Rows only qualify a table in the Running phase, so checksum
and cutover phases keep their state phrases.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@aparajon
aparajon force-pushed the armand/strata-live-table-progress branch from d0aa6b4 to e03e49e Compare August 31, 2026 16:41
Copilot AI lite review requested due to automatic review settings August 31, 2026 16:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enhances sharded apply PR-comment UX by aggregating per-shard row-copy metrics (rows copied/total + ETA) into the per-table rollup line, so operators can see live copy progress at the table level instead of only a generic state phrase.

Changes:

  • Aggregate RowsCopied, RowsTotal, and ETASeconds across shard tasks when building sharded apply template data.
  • Render a per-table progress bar + rows/ETA line for actively copying sharded tables (keeping non-copy phases on state phrases).
  • Add/adjust previews and tests to cover the new rendering and edge cases (starting copy, estimate exceeded, non-copy phases).

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
TEMPLATES.md Updates rendered sharded-apply examples to show table-level copy progress (bar + rows/ETA) and adjusts cancelled glyph in the example output.
pkg/webhook/templates/sharded_apply.go Adds aggregated row-copy fields to ShardedTableStatus and renders a table-level progress bar + rows/ETA when copying.
pkg/webhook/templates/sharded_apply_test.go Adds template-level tests covering progress rendering, starting-copy, estimate-exceeded, and gating rows to the copy phase.
pkg/webhook/templates/preview_sharded.go Updates sharded preview fixtures to include row-copy metrics so previews match the new renderer.
pkg/webhook/sharded_apply.go Aggregates shard task progress into per-table rollups (sum rows, max ETA) at render-data build time.
pkg/webhook/sharded_apply_test.go Updates/extends builder tests to assert row/ETA aggregation behavior.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread pkg/webhook/templates/sharded_apply.go Outdated
A large table early in its copy has rows flowing but an integer fraction
that rounds down to zero, which rendered the starting indicator as if no
rows had been copied. Route the percent through ui.RowCopyDisplayPercent,
matching the single-target renderer, so the starting form is reserved for
a copy that has not reported rows yet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@aparajon
aparajon marked this pull request as ready for review August 31, 2026 16:54

@morgo morgo left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Approved on Morgan's behalf by his AI agent.

Comment-rendering only, 37/37 green, and it reuses the single-deployment honesty guards (EstimateExceeded → "Finalizing copy", pct == 0 → "Starting copy...") rather than reinventing them. The division is safe — t.RowsTotal > 0 gates the whole branch — and shardTaskProgress replacing best wholesale rather than field-by-field is the right call, since it can't splice one task's status onto another's row counts.

Approving because this is strictly better than showing no progress at all. But two of the aggregated figures claim more than they can deliver once a rollout spans more than one dispatch wave, and I'd rather flag them now than have someone file them as bugs later.

1. The percentage regresses as waves start. RowsTotal sums only the shards that have reported. A 4-shard table with wave 1 in flight renders 500k / 1M = 50%; when wave 2 starts and adds its shard's total, the same table renders 500k / 2M = 25%. The bar goes backwards, in a comment that's edited in place, while the operator is watching. The doc comment frames the growing total as a feature — "live, not a plan-time promise" — which is a fair description of the mechanism but doesn't acknowledge that the rendered consequence is a progress bar that visibly loses ground.

2. The ETA is the slowest started shard, not the slowest shard. Unstarted shards contribute etaSeconds: 0, so max skips them entirely. During wave 1 of 4, the line reads as the table's ETA but is really wave 1's ETA — it will systematically under-report, and it under-reports most severely at the start, which is when people read it. "ETA 3 minutes" on a table with three waves still to dispatch is the kind of thing that costs the tool credibility.

Neither is hard to soften if you want them: denominating the percentage over all shards (treating unreported shards as unknown rather than zero), or suppressing the ETA line until every shard has reported, would both make the number mean what the label says. A cheaper option is to keep the figures and qualify the label while waves are outstanding — e.g. "ETA 3m (shards still dispatching)" — so the number stays useful without overclaiming.

Nit: the preview fixture bumps -40 from 45% to 62% alongside the new row figures. Worth a sanity check that the fixture's shard percent and the new RowsCopied/RowsTotal (914707/1466232 ≈ 62%) are meant to agree — they do here, which is a nice touch, but nothing enforces it, so a future edit to one can silently desync the preview from what the real renderer would produce.

The summed rows and slowest-shard ETA cover only the shards whose engine
has reported figures. Before every dispatch wave has started, the plain
rows line presented a wave's fraction as the whole table's: the bar lost
ground when the next wave's total joined the denominator, and the ETA
read as the table's while describing only the started shards. Carry the
reporting-shard count on the rollup and, while it trails the shard count,
render the coverage on the rows line and the ETA as a floor. The preview
fixture's shard percent is now derived from its row figures so the two
cannot drift apart.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@morgo morgo left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Re-approved on Morgan's behalf by his AI agent, now at 91d28bcc. My earlier approval was on 496bd538; re-stamping so it covers the code that's actually here.

Reviewed the delta against the two things I raised.

ETA — fully fixed. ETA: ≥ 3m while ShardsReporting < len(Shards) is exactly right. The unstarted shards can only add rows and time, so a floor is the honest form, and communicates it without a sentence of explanation.

Fixture drift — fixed, and better than I suggested. Deriving previewMutesCopyPercent from the row constants means the summary line and rows line can't desync. Reused in the divergent fixture too.

Percentage regression — disclosed, not resolved. The rows line now says "across 1 of 4 shards", which gives an operator what they need to interpret the number. But the bar itself is unchanged:

pct := ui.RowCopyDisplayPercent(int(ui.ClampRows(t.RowsCopied, t.RowsTotal)*100/t.RowsTotal), t.RowsCopied)

RowsTotal still covers only reporting shards, so a 4-shard table still renders 62% on wave 1 and still drops to roughly 31% when wave 2 starts. The struct comment now says the rows line "discloses partial coverage rather than passing a wave's fraction off as the table's" — but the bar is the loudest element on the line and it is still the wave's fraction presented as the table's. The caveat sits on the quieter line beneath it.

Not blocking, because disclosure is a real improvement over silence and the alternative costs more than it's worth mid-review. If you want to close it later, qualifying the headline the same way the rows line is qualified would do it — e.g. 62% (1 of 4 shards), or holding the bar back until every shard reports and showing the phase word until then.

Two small things in the new code, neither blocking:

  • writeShardedRowsAndETA's t.RowsTotal <= 0 early return is unreachable — writeShardedTableLine already gates the whole branch on t.RowsTotal > 0. Harmless as defensive code, just noting it isn't load-bearing.
  • shardsReporting increments only when sp.rowsTotal > 0, while r.rowsCopied += sp.rowsCopied is unconditional. A shard reporting copied rows with a zero total would add to the numerator without the denominator or the count — which lands in EstimateExceeded → "Finalizing copy". That's a sane place to land, so I think it's fine; flagging in case that combination is supposed to be impossible rather than merely handled.

While later dispatch waves have yet to report, the summed fraction
behind the progress bar covers only the reporting shards; the headline
now says so — "62% (1 of 4 shards)" — matching the rows line, and the
finalizing form carries the same qualifier. Aggregation gates all of a
shard's copy figures on a reported row total, so the numerator,
denominator, ETA, and coverage count always describe the same shards.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@morgo morgo left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Re-approved on Morgan's behalf by his AI agent — his approval was at 91d28bcc and the head has moved to 2ff707ab. Re-verified rather than left to ride, and it's worth saying why: the new commit fixes a real bug that was present in the version he approved.

At 91d28bcc, shardedTableStatusesByKeyspace gated r.shardsReporting++ on sp.rowsTotal > 0 but accumulated the figures unconditionally:

if sp.rowsTotal > 0 {
    r.shardsReporting++
}
r.rowsCopied += sp.rowsCopied      // ran regardless
r.rowsTotal  += sp.rowsTotal

So a shard reporting copied rows before it had a row total contributed to the numerator without contributing to the denominator or to the coverage count — inflating the displayed percentage, and able to push it past 100% in the right ordering. 2ff707ab moves all four accumulations inside the same guard, so the numerator, denominator, ETA, and coverage count always describe the identical shard set. That's the correct invariant and the comment states it precisely.

The other two changes hold up as well:

  • The headline now carries the coverage qualifier (62% (1 of 4 shards)), matching what the rows line already disclosed. Consistent, and it closes the gap where the bar could be read as the whole table's progress while later dispatch waves hadn't started.
  • Dropping the if t.RowsTotal <= 0 { return } guard from writeShardedRowsAndETA is a consequence of the fix, not an oversight. Before, a zero total could coexist with copied rows, and the guard suppressed the line. Now a zero total means nothing is reporting at all — and in that state the coverage disclosure ("0 of 4 shards") is the informative thing to print, not something to hide.

CI green at the new head.

@aparajon

aparajon commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator Author

🤖 Addressing morgo's re-review — all three items landed in 2ff707a.

Percentage regression: took your suggestion directly — the headline now carries the same qualifier as the rows line (62% (1 of 4 shards), reusing the plan comment's coverage phrasing), and the finalizing form is qualified the same way, so the loudest element no longer presents a wave's fraction as the table's. Unreachable guard: removed — both call sites sit behind the RowsTotal > 0 gate. Copied-rows-without-a-total: that combination isn't meant to be impossible (an engine can report copied rows before an estimate), so it's now handled consistently instead of accidentally — all of a shard's figures are gated together on it reporting a total, meaning the numerator, denominator, ETA, and coverage count always describe the same set of shards. A test pins it.

— Claude (claude-fable-5) via Claude Code

@aparajon
aparajon merged commit 281cb47 into main Sep 1, 2026
38 checks passed
@aparajon
aparajon deleted the armand/strata-live-table-progress branch September 1, 2026 15:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants